using System;
using System.Collections.Generic;
using System.Diagnostics;
using ZoneFiveSoftware.Common.Data;
using ZoneFiveSoftware.Common.Data.Fitness;
using FilteredStatistics.Common;

namespace FilteredStatistics.Data
{
    class BasicFilteredDataSet : IFilteredDataSet
    {
        public BasicFilteredDataSet(IActivity activity, Filter filter, FilterResults timesToFilter)
        {
            Debug.Assert(activity != null && filter != null);

            BuildFilteredTimes(activity, filter, timesToFilter);
        }

#region IFilteredDataSet Members

        public FilterResults GetFilteredTimes()
        {
            return m_FilteredRanges;
        }

#endregion

        protected virtual void BuildFilteredTimes(IActivity activity, Filter filter, FilterResults timesToFilter)
        {
            FilterResults filterMatchingTimes;

            filterMatchingTimes = BuildValidTimes(activity, filter);
            m_FilteredRanges = IntersectTimeRanges(timesToFilter, filterMatchingTimes);
        }

        protected virtual FilterResults BuildValidTimes(IActivity activity, Filter filter)
        {
            if (filter.Zones.Count == 0)
            {
                return WholeActivityFilteredDataSet.Instance.GetFilteredTimes();
            }
            else
            {
                List<FilterResults> validTimesToMerge = new List<FilterResults>();

                foreach (INamedZone currentSelectedZone in filter.Zones)
                {
                    validTimesToMerge.Add(new FilterResults(currentSelectedZone.ValidTimes));
                }

                return MergeTimeRanges(validTimesToMerge);
            }
        }

        protected virtual FilterResults MergeTimeRanges(List<FilterResults> sourceValidTimes)
        {
            FilterResults validTimes = new FilterResults();
            List<FilterResults> validSeries = new List<FilterResults>();

            foreach (FilterResults serie in sourceValidTimes)
            {
                if (serie.Count > 0)
                {
                    validSeries.Add(serie);
                }
            }

            if (validSeries.Count == 1)
            {
                validTimes = validSeries[0];
            }
            else if (validSeries.Count > 1)
            {
                List<int> timeMarkersList = new List<int>();

                int currentZoneIndex = -1;
                DateTime activityStartTime = DateTime.MaxValue;
                DateTime activityEndTime = DateTime.MinValue;
                DateTime currentStartMarker = activityStartTime;
                DateTime currentEndMarker = currentStartMarker;

                // Find our earliest and latest time overall
                int i = 0;
                foreach (FilterResults valueRange in validSeries)
                {
                    // Remember our last time marker for this zone
                    timeMarkersList.Add(0);

                    if (valueRange.Count > 0)
                    {
                        if (activityStartTime > valueRange[0].Lower)
                        {
                            activityStartTime = valueRange[0].Lower;
                            currentEndMarker = valueRange[0].Upper;
                            currentZoneIndex = i;
                        }
                        // Check if we found a zone with same start but later end
                        else if (activityStartTime == valueRange[0].Lower &&
                                 currentEndMarker < valueRange[0].Upper)
                        {
                            // Choose this zone instead as it encompasses our previous result
                            currentEndMarker = valueRange[0].Upper;
                            currentZoneIndex = i;
                        }

                        if (activityEndTime < valueRange[valueRange.Count - 1].Upper)
                        {
                            activityEndTime = valueRange[valueRange.Count - 1].Upper;
                        }
                    }

                    ++i;
                }

                // Make sure we found our starting zone
                Debug.Assert(currentEndMarker != currentStartMarker);

                currentStartMarker = activityStartTime;

                bool allMarkersAtEnd = false;
                while (currentEndMarker < activityEndTime && !allMarkersAtEnd)
                {
                    int nextZoneIndex = currentZoneIndex;
                    IValueRange<DateTime> nextRange = null;

                    allMarkersAtEnd = true;

                    int j = 0;
                    foreach (FilterResults valueRange in validSeries)
                    {
                        int timeMarkerIndex = timeMarkersList[j];

                        // Advance our marker up to our current end marker
                        while (timeMarkerIndex < valueRange.Count && valueRange[timeMarkerIndex].Lower <= currentStartMarker)
                        {
                            timeMarkerIndex++;
                        }

                        // Save our new marker position for this zone
                        timeMarkersList[j] = timeMarkerIndex;

                        if (timeMarkerIndex < valueRange.Count)
                        {
                            allMarkersAtEnd = false;

                            IValueRange<DateTime> currentRange = valueRange[timeMarkerIndex];

                            if (currentRange.Lower == currentEndMarker)
                            {
                                // We found a matching next zone
                                nextZoneIndex = j;
                                nextRange = currentRange;
                            }
                            else if (currentRange.Lower < currentEndMarker &&
                                    currentRange.Upper > currentEndMarker)
                            {
                                // We found a matching overlapping zone
                                nextZoneIndex = j;
                                nextRange = currentRange;
                            }
                        }

                        ++j;
                    }

                    if (!allMarkersAtEnd)
                    {
                        // We haven't found a consecutive valid zone, so we hit the end of the current valid range
                        if (nextZoneIndex == currentZoneIndex)
                        {
                            validTimes.Add(new ValueRange<DateTime>(currentStartMarker, currentEndMarker));

                            // Find our next valid range
                            int k = 0;
                            DateTime tempStartMarker = DateTime.MaxValue;
                            foreach (FilterResults valueRange in validSeries)
                            {
                                int currentTimeMarker = timeMarkersList[k];

                                if (currentTimeMarker < valueRange.Count)
                                {
                                    if (tempStartMarker > valueRange[currentTimeMarker].Lower)
                                    {
                                        nextRange = valueRange[currentTimeMarker];
                                        tempStartMarker = nextRange.Lower;
                                        nextZoneIndex = k;
                                    }
                                    // Check if we found a zone with same start but later end
                                    else if (tempStartMarker == valueRange[currentTimeMarker].Lower &&
                                             currentEndMarker < valueRange[currentTimeMarker].Upper)
                                    {
                                        // Choose this zone instead as it encompasses our previous result
                                        nextRange = valueRange[currentTimeMarker];
                                        nextZoneIndex = k;
                                    }
                                }

                                ++k;
                            }

                            currentStartMarker = nextRange.Lower;
                        }

                        currentEndMarker = nextRange.Upper;
                        currentZoneIndex = nextZoneIndex;
                    }
                }

                // We reached the end, add the last valid zone
                validTimes.Add(new ValueRange<DateTime>(currentStartMarker, currentEndMarker));
            }

            return validTimes;
        }

        protected virtual FilterResults IntersectTimeRanges(FilterResults sourceTimes1, FilterResults sourceTimes2)
        {
            FilterResults result = new FilterResults();
            int source1Marker = 0;
            int source2Marker = 0;

            while (source1Marker < sourceTimes1.Count && source2Marker < sourceTimes2.Count)
            {
                IValueRange<DateTime> currentSource1Time = sourceTimes1[source1Marker];
                IValueRange<DateTime> currentSource2Time = sourceTimes2[source2Marker];

                // Seek up to the point where we passed our current valid range start time
                while (currentSource1Time != null &&
                       currentSource2Time.Lower > currentSource1Time.Upper)
                {
                    ++source1Marker;

                    if (source1Marker < sourceTimes1.Count)
                    {
                        currentSource1Time = sourceTimes1[source1Marker];
                    }
                    else
                    {
                        currentSource1Time = null;
                    }
                }

                if (currentSource1Time != null)
                {
                    if (currentSource2Time.Upper < currentSource1Time.Lower)
                    {
                        // We hit a valid section that has no matching range in the source, skip it
                        ++source2Marker;
                    }
                    else
                    {
                        // We have found a matching range, we must choose the right
                        //  lower & upper bounds
                        ValueRange<DateTime> newResultRange = new ValueRange<DateTime>(new DateTime(Math.Max(currentSource1Time.Lower.Ticks, currentSource2Time.Lower.Ticks)),
                                                                                       new DateTime(Math.Min(currentSource1Time.Upper.Ticks, currentSource2Time.Upper.Ticks)));
                        result.Add(newResultRange);

                        if (newResultRange.Upper == currentSource1Time.Upper)
                        {
                            ++source1Marker;
                        }

                        if (newResultRange.Upper == currentSource2Time.Upper)
                        {
                            ++source2Marker;
                        }
                    }
                }
            }

            return result;
        }

        private FilterResults m_FilteredRanges = new FilterResults();
    }
}
